home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Pascal Super Library
/
Pascal Super Library (CW International)(1997).bin
/
DELPHI32
/
SYS_TOOL
/
MULTI020
/
MULTI020.ZIP
/
MULTI.DOC
< prev
next >
Wrap
Text File
|
1993-09-08
|
27KB
|
816 lines
|
|
MULTI |
|
a Borland Pascal unit for cooperative multitasking |
|
version 0.2 beta |
|
(C) copyright 1993 by Felix von Leitner |
|
written in 1993 by Felix von Leitner |
leitner@inf.fu-berlin.de |
|
|
Contents
What Is Multitasking ?
Is MULTI For Me ?
Does Not Multitasking Create Many New Problems ?
How Does Multitasking PASCAL Code Look Like ?
Advanced Multitasking - Semaphores
Interprocess Communications Via Pipes
Things I Have Forgotten So Far
Chapter 1: What Is Multitasking ?
Multitasking comes from "multi" and "task", multi being the Latin word
for "many" and "task" meaning any piece of work. Multitasking means
that your procedures can run parallelly, and they can interfere with
each other, if you want them to.
MULTI is a pure software solution. MULTI can't do miracles, if one task
crashes, the whole program crashes. MULTI won't change your program
into parallel wonders without some work.
In my opinion multitasking is rather a programming style than a unit,
but you have to have some tools to do it efficiently. MULTI provides
you with these tools.
Chapter 2: Is MULTI For Me ?
MULTI is not for beginners. I am using Turbo and Borland Pascal for
several years now, I am an experienced Assembler programmer, and I have
lots of fun with MULTI. You don't have to be any of these, you don't
have to be much experienced with Borland Pascal, but you have to be an
experienced programmer. If you are an experienced C or BASIC progammer,
you will have little problems with Borland Pascal, and as I said above,
MULTI is just the tools for your multitasking programming. So I will
have to explain what multitasking programming means and not only give a
reference of the function in MULTI.
An integral part in writing programs is going from spaghetti code where
all data is globally accessible to local data and encapsulation. In my
opinion OOP is a great step, it is not the solution to the problems of
the world, but it helps a lot. You have to have understood what data
encapsulation does for you to be able to enjoy multitasking. You can
have much fun with MULTI if you write small programs, but the goal is
to create big applications more easily, and for big applications data
encapsulation is VERY important.
Data encapsulation means that part A of your program cannot access the
data of part B if both parts don't have anything to do with each other.
Now you not only have several parts of your program, you have several
TASKS, several procedures running in parallel, it is ABSOLUTELY FATAL
if they interfere in each other private data without some insulation. I
have tried to provide some means of general insulation with PIPES, but
like the whole unit, the pipes are not completely finished yet.
I want you to understand that multitasking can radically restructure
your world of programming, it can be a great help (and a boost in
programming time) if you really think your concepts to the end.
One example of trivial multitasking is a jump and run game, where all
the sprites have their own task which directs them. Another example is
a printer spooler or a serial communications spooler. Or a disk cache
which works in the background ! Everything is possible. Or you have a
task which reorganizes your data if nothing else is done. Your database
creates a report while the user does something else !
Chapter 3: Does Not Multitasking Create Many New Problems ?
If you think of "real" multitasking there would be many new problems.
No task could be sure no other task has changed their variables in the
midst of a calculation ! But MULTI uses a different approach,
cooperative multitasking. That means, each task does the switching, so
you can switch when the task is ready to be interrupted.
Chapter 4: How Does Multitasking PASCAL Code Look Like ?
Well, I don't want to show you real multitasking code, I want to
demonstrate how MULTI grew according to my needs. My first idea was to
just have a procedure which runs while the main program runs, too.
│ uses crt, multi; { This example does NOT work, see below ! }
│
│ procedure task; far;
│ begin
│ repeat
│ writeln('Task running !');
│ until false;
│ end;
│
│ begin
│ Fork(task); { Activate the task running in the background }
│ end.
Look nice, but is not sufficient. Several problem are there. MULTI does
not do miracles, it won't switch tasks automatically, you have to do
that. The procedure for that is called Switch.
│ uses crt, multi; { This does not work, too. See below ! }
│
│ procedure task;
│ begin
│ repeat
│ writeln('Task running !');
│ Switch; { Switch away }
│ until false
│ end;
│
│ begin
│ Fork(task);
│ repeat
│ Switch
│ until keypressed;
│ end.
This is closer to the real MULTI, but it ain't enough neither. You
could write program using this method, but the next problem is : Can
you use local variables in the task procedure ? Yes you can. Can you
use global variables from within it ? Yes you can. You can call other
procedures, you can even call recursive procedures (procedures which
call themselves), if you understand the mechanism of procedure calling
and local procedures, you know that these use up stack. But MULTI can't
do miracles, it does not know how much stack it should allocate for the
task, the task cannot use the main stack (the only one Borland Pascal
knows of), because the main task uses it, and no two tasks may disturb
each other's stack. So MULTI has to allocate memory from the heap and
give it to each task procedure as stack. The procedure won't know which
stack is uses, and it does not matter. So you have to tell FORK how
much stack to give the task. The next version of the program looks like
that :
│ uses crt, multi; { Still does not work, but gets closer }
│
│ procedure task; far;
│ var i : word
│ begin
│ for i := 1 to 1000 do begin
│ writeln('In task !');
│ Switch
│ end
│ end;
│
│ begin
│ Fork(task,2048);
│ repeat
│ Switch
│ until keypressed
│ end.
All right, even closer to a real program. 2048 is a fine stack space if
you don't call other procedures which call other procedures which call
other ... you get the idea. 2048 is enough for the procedure above. You
can see that the task now has no endless loop in it. Mhh, what happens
when it exits ? Maybe your system crashes ? No problem with MULTI ! The
task simply terminates. That means, calls to SWITCH won't activate the
task again, it's stack space is given back to the heap again, the local
variables are gone. The above program prints up to 1000 'In task !' if
you don't press a key before that, then it exits.
In the above examples I assumed that if the main program exits (maybe
through a run-time error) no task is called again. If a task has opened
a file and just wanted to save some important data, the data are not
saved. That is not desireable. But more about that later. Now I want a
task that prints 'Task A !' and one task which prints 'Task B !'. How
could I do that ? You have to give the task a parameter. But you don't
call the task, FORK does ! Mhh, so you have to tell Fork what to give
the task as parameters. Since I don't want a Fork for procedures
without parameters, and one for every possible number of parameters, I
write a Fork which takes one argument (an untyped VAR parameter). So
the program looks like this :
│ uses crt, multi; { That's it ! }
│
│ procedure task(var m); far;
│ var
│ s : string absolute m;
│ i : word;
│ begin
│ for i := 1 to 1000 do begin
│ writeln(s);
│ switch
│ end
│ end;
│
│ var
│ s : string;
│
│ begin
│ s := 'Task A !';
│ Fork(task,2048,s);
│ s := 'Task B !';
│ Fork(task,2048,s);
│ repeat
│ switch
│ until keypressed
│ end.
What do you think this code does ? It prints 'Task A !', then 1999
times 'Task B !' ! You see one problem with the parameters, they are
pointer. Both tasks get the address of the same variable, so if we want
one task printing 'Task A !' and one printing 'Task B !', we could have
the task procedures save the string to a local variable or we can have
the main program allocate space for the task name on the heap and
copying the wanted string there, or we could have s and s1 as strings,
one containing 'Task A !' and one 'Task B !'. That's a drawback, right.
But what can I do ? What if I want to pass TWO parameters to a task ?
You have to define a record and pass that. Sorry for the inconvenience,
but the result rewards you.
Now to the other problems. You have seen that you can terminate a task
with exit. This is a hack for convenience, and you should use it, but
historically there is another way to do that : call Terminate. It frees
the stack and SWITCHes to the next task. If no tasks are there, the
program ends. If you call Terminate from the main program, it is
treated as task and terminated, the other tasks still run. If you wrote
a task which has an endless loop in it and terminate the main program,
your program runs forever !
What happens if the main task ends by reaching the 'end.' and not by
Terminate ? You should think that all tasks are terminated then, since
noone calls SWITCH again. And that's the way it was. And it's still
so in a way. These tasks are nice, but if you want to do something from
the real world with them, you fail. Because they can't react to errors,
if anybody produces a run-time error, no task is called again. Now
imagine a task doing serial communications. That task has to setup some
interrupts and it HAS to de-initialize them or the system will crash.
Normally one would use the EXITPROC mechanism for that, but EXITPROC is
global, and we want to encapsulate the tasks, we want to insulate them
from each other (the main task being just one of the tasks). So each
procedure should have something EXITPROC like. The simples way I could
imagine is by letting SWITCH return a boolean value. FALSE means that
everything is still fine, TRUE means that we have an error condition
and must deinit and terminate NOW. But not every task needs that extra
treatment. Some tasks can be killed directly, and we don't want to
complicate matters by having those tasks react to SWITCH's result. If
SWITCH wants to switch to such a task under an error condition that
task is simply killed. That sounds incredibly difficult, this example
illustrates everything :
│ uses crt, multi;
│
│ procedure standard_task(var m); far;
│ begin
│ repeat
│ writeln('This task does not deinitialize');
│ Switch;
│ until false;
│ writeln('This is never written')
│ end;
│
│ procedure new_task(var m); far;
│ begin
│ t^.hasexit := true;
│ { Tell MULTI we want to deinitialize ! }
│ repeat
│ writeln('This task DOES deinitialize');
│ if Switch then break; { break goes behind the "until false" }
│ until false;
│ writeln('This is the deinitialization');
│ end;
│
│ begin
│ Fork(standard_task,2048,Nothing);
│ Fork(new_task,2048,Nothing);
│ end.
'Nothing' is a dummy variable declared in MULTI for Forks to tasks
which have no real parameters. Normally all the tasks are treated as
the first task, they are simply terminated if the program HALT()s or
RunError()s. But if a task (who else can know if the task wants to
deinitialize ?) wants to deinitialize, it has to tell MULTI by setting
t^.hasexit to TRUE. 't' is a global pointer in MULTI which points to
the current task, that means if the main task says t^.hasexit:=false,
it is terminated if a task produces a run-time error. The main task has
an deinitialization by default.
Chapter 5: Advanced Multitasking - Semaphores
Hey cool, semaphore. Another word you have never heard of. (If English
is your native language, you know what that is, don't be insulted. This
document covers Germans, too, and most Germans don't know what a sema-
phore is)
It does not matter if you have heard of that before, I will explain the
concept of them now. Here's what my lexicon says : "system of sending
signals by holding the arms or two flags in certain positions to
indicate letters of the alphabet" and "device with red and green lights
on mechanically moved arms, used for signalling on railways."
Multi's semaphores are more like the latter definition. While it is
essential for two tasks to be insulated from each other, they need some
way of communicating with each other. As example I present two tasks
counting from 1 to 1000, but the second wants to wait until the first
one has counted to 500. The first approach would be a global variable :
│ uses multi;
│
│ var
│ task_2_should_start_now : boolean;
│
│ procedure task_1(var m); far;
│ var w : word;
│ begin
│ task_2_should_start_now := false;
│ w := 1;
│ repeat
│ inc(w);
│ if w = 500 then task_2_should_start_now := true;
│ Switch
│ until w = 1000;
│ end;
│
│ procedure task_2(var m); far;
│ var w : word;
│ begin
│ repeat Switch until task_2_should_start_now;
│ w := 1;
│ repeat
│ inc(w);
│ Switch
│ until w = 1000;
│ end;
│
│ begin
│ Fork(task_1,2048,Nothing);
│ Fork(task_2,2048,Nothing);
│ Terminate
│ end.
This should do it, but it violates our Prime Directive (yeah, I am yet
another Trekkie) of not interfering with other tasks. Why yield to
crusty principles, you might ask. Well, that is not just a principle,
it is the only (yes, THE *ONLY*) way to make your program's behaviour
unterstandable for mere humans. If you have several tasks interfering
with each other by means of some global variables, you will soon die of
an heart attack. By the way, you have read right, you can still debug
your multitasking programs ! From within the IDE and with Turbo
Debugger, everything works fine ! You can even F8 over SWITCH and all
the other tasks will then execute and you will be in your task again !
So how should two tasks communicate without global variables ? Yeah,
right, that won't work. I present semaphores as solution to the
problem, but semaphores are variables, too. You can't use the Vulcan
Mind Touch, so for now think of semaphores as syntactical sugar for
booleans. The syntax is this :
│ uses multi;
│
│ var
│ task_1_is_ready : semaphore;
│
│ procedure task_1(var m); far;
│ var w : word;
│ begin
│ InitSemaphore(task_1_is_ready);
│ w := 1;
│ repeat
│ inc(w);
│ if w = 500 then
│ Release(task_1_is_ready);
│ Switch
│ until w = 1000;
│ end;
│
│ procedure task_2(var m); far;
│ var w : word;
│ begin
│ WaitFor(task_1_is_ready);
│ w := 1;
│ repeat
│ inc(w);
│ Switch
│ until w = 1000;
│ end;
│
│ begin
│ Fork(task_1,2048,Nothing);
│ Fork(task_2,2048,Nothing);
│ Terminate
│ end.
This does not only look different, it is more efficient, too, since
MULTI does not switch to tasks waiting for a semaphore. It complicated
MULTI's code considerably, but it is much nicer. Tasks waiting for a
semaphore with t^.hasexit=TRUE are still not terminated, but WAITFOR is
a function, too. Instead of asking if SWITCH is true you can now ask if
WAITFOR is true. If WAITFOR is true, you should deinit immediately.
Of course MULTI can handle several tasks waiting for a semaphore.
There are situations where you want to kill tasks which wait for a
semaphore, because you know the semaphore will never be released. In
that case, MULTI offers the procedure KAMIKAZE, which terminates (or
lets deinitialize) all waiting tasks.
Mhh, you see and understand that code, but you can't understand what
those semaphores are good for ? They are needed for the next way of
interprocess communication, pipes.
Chapter 6: Interprocess Communications Via Pipes
What do I mean with 'pipe'...? 'pipe' is a metaphora for something long
and dark where you put in something on one side and it comes out on the
other. The important point about it is that one side does not know
about the other side. That sounds superfluous, doesn't it ? But it is
not. Imagine you write a program which reads from a pipe and displays
the result in a Turbo Vision window. Or a Windoze window. Or wherever.
Then you only have to implement new pipes to expand the program. You
could write tasks which read from a pipe and write to the modem. You
could write error correcting pipes and just have tasks communicate via
pipes, and they don't even have to run on the same computer, they can
communicate via modem, network etc. and the tasks wouldn't know it. Any
with semaphores the pipes become efficient, since we can fully make use
of MULTI's possibilities. If a task reads from a pipe and there are no
data in the pipe, that task is put "asleep" waiting for a semaphore.
You can have more than one task read from a pipe, but that does not
make sense. It may make sense to have more than one task write to a
pipe, MPIPES is able to do that, anyway.
So the next goal is to write tasks which do device dependent in- and
output to and from pipes. Have a look into the units MPKBD, MPTTYCRT,
MPLOOP and MPFOSSIL for such tasks (I call them "clients", since they
don't do much, they only provide an interface).
When initializing a pipe you have to say how much bytes buffer you want
to have. Then you can put a byte into it with Put, get a byte out of it
with Get, put or get several bytes with PutBin and GetBin, or close it
with Done. Some example code :
│ uses multi, mpipes;
│
│ var
│ t : tPipe;
│ s : string;
│
│ procedure GetTask(var m); far;
│ var s : string;
│ t : tpipe absolute m;
│ begin
│ s[0] := t.Get; { Read length byte }
│ t.GetBin(s[1],length(s)); { Then the rest of string }
│ writeln('GetTask received: ',s);
│ Terminate;
│ end;
│
│ begin
│ s := 'Hallo, Welt';
│ t.Init(10); { Open pipe with 10 bytes buffers }
│ Fork(GetTask,4096,t,'GetTask');
│ { Create task which reads a string from the pipe }
│ t.PutBin(s,length(s)+1); { Write a string to the pipe }
│ Terminate;
│ { Terminate this task; let GetTask display the string }
│ end.
Two things are noteworthy about this code : First the pipe is smaller
than the data put into it by PutBin. That means, the pipe handles that
for you, it puts your data into the pipe and returns after that. In the
meantime it may have been put asleep several times and you wouldn't
notice it !
The second thing is that Fork gets a fourth parameter here, a string.
This is a debug mode I have implemented in MULTI. Programming it is
much harder that programming *for* it. That's why I wrote that debug
mode. Just {$DEFINE DEBUG} when compiling MULTI and the other units and
then several things change. Fork gets a string as fourth parameter, you
get debug dumps on your monochrome monitor (if you have one, otherwise
your system will crash ;) or you adjust DUAL.PAS), and all major
actions about tasks are reported on the monochrome monitor. If you
want the dumps (a list of all active tasks) just debugdump:=true, if
you want the actions logged on the monochrome screen just debug:=true.
This can be a great tool for problem diagnosis ("Why doesn't that task
read the pipe ?").
For your (and mine) convenience I have written PutS and GetS methods
which write and read strings to pipes (first the length byte, then the
data). So if both tasks know what they are doing, you can use these,
too.
Also, I have implemented ReadLn and WriteLn methods for strings from
and to pipes, they append the usual DOS #13#10 pair to the string, then
put it. This simplifies writing of a Terminal through pipes. I plan to
write a CRT replacement through pipes with a Terminal emulation. Don't
know how soon I even start that.
To test that ReadLn and WriteLn I wrote a new pipe client to and from
files (Unit MPFile). When doing that I discovered another thing I had
forgot about the pipes. What if you set up a task which does input from
a pipe and a task which does output, and the output task fails ? The
input task is put asleep and will never awaken. The pipe will stay
around and waste memory. So the next step in pipe development is what I
call 'broken pipes'. I call it that way because Linux writes that in
situations like those I believe. But how should the pipe know that
something is wrong ? Each client now has to register with the pipe
(with the new pipe methods NewInputTask and NewOutputTask) and check
out (NoMoreInput and NoMoreOutput). If the last task of a kind checks
out and the other kind waits it is terminated. If the other kind does
not wait, it is killed when it starts waiting. Then the pipe memory is
released.
Appendix A: Things I Have Forgotten So Far
All the examples in this manual and MULTI and the accompanying units
assume you have "extended syntax" enabled by default {$X+}.
Fork is a function, too, and returns a pointer to the new task. This
pointer can be used to make that task wait for a semaphore or to kill
that task.
A task is killed by setting the boolean Poisoned to TRUE, that's where
the pointer Fork returns comes in handy. So a task kills itself with
"t^.Poisoned := true". You can kill other tasks by saving what Fork
returns and then setting fuckup :
var t : semaphore;
{ some code ... }
begin
t := Fork(mytask,2048,Nothing,'Dies young');
{ do something ... }
t^.Poisoned := true;
{ The next switch will kill or deinit that task }
Kamikaze(t) works, and kills all active tasks, so that's probably not
what you wanted.
If your program dies and you don't know why, and it does not always die
in the same situation, some task probably has to little stack space. Or
your pointers have gone wild (that's no problem of MULTI). Or MULTI is
broken, please contact me if you are sure that the latter is true !
As many other sources I have written MULTI could rot on my hard disk,
or it can go all around the world and cause great grief to people who
try to work with it. Maybe it is helpful to somebody, any I hope it is
as helpful to you as it is to me ! This is just the very beginning, the
new possibilities are overwhelming. You can put almost anything through
a pipe, and you can route a pipe everywhere with a little work. Queries
to the database could work locally, through a network or via modem, and
neither the user nor the program would care. A terminal could operate a
remote modem somewhere in a network. You can simply simulate input to a
program via disk pipes. I could continue endlessly !